/*
* Copyright (C) 2010 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.launcher2;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ValueAnimator;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.AttributeSet;
import android.util.Log;
import android.view.InputDevice;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.view.ViewParent;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityManager;
import android.view.accessibility.AccessibilityNodeInfo;
import android.view.animation.Interpolator;
import android.widget.Scroller;
import com.android.launcher.R;
import java.util.ArrayList;
/**
* 支持分页查看的workspace的一个抽象
*/
public abstract class PagedView extends ViewGroup implements
ViewGroup.OnHierarchyChangeListener {
private static final String TAG = "PagedView";
private static final boolean DEBUG = false;
protected static final int INVALID_PAGE = -1;
// 达成fling所需最小的距离,防止一不留神滑页了
private static final int MIN_LENGTH_FOR_FLING = 25;
protected static final int PAGE_SNAP_ANIMATION_DURATION = 550;
protected static final int MAX_PAGE_SNAP_DURATION = 750;
protected static final int SLOW_PAGE_SNAP_ANIMATION_DURATION = 950;
protected static final float NANOTIME_DIV = 1000000000.0f;
private static final float OVERSCROLL_ACCELERATE_FACTOR = 2;
private static final float OVERSCROLL_DAMP_FACTOR = 0.14f;
// 返回原桌面页的滑动宽度阀值
private static final float RETURN_TO_ORIGINAL_PAGE_THRESHOLD = 0.33f;
// 进入下一桌面页的滑动宽度阀值
private static final float SIGNIFICANT_MOVE_THRESHOLD = 0.4f;
// 下面的常量将会根据密度修正,修正的数值将赋给下面的变量
private static final int FLING_THRESHOLD_VELOCITY = 500;
private static final int MIN_SNAP_VELOCITY = 1500;
private static final int MIN_FLING_VELOCITY = 250;
static final int AUTOMATIC_PAGE_SPACING = -1;
protected int mFlingThresholdVelocity;
protected int mMinFlingVelocity;
protected int mMinSnapVelocity;
protected float mDensity;
protected float mSmoothingTime;
protected float mTouchX;
protected boolean mFirstLayout = true;
protected int mCurrentPage;
protected int mNextPage = INVALID_PAGE;
protected int mMaxScrollX;
protected Scroller mScroller;
private VelocityTracker mVelocityTracker;
private float mDownMotionX;
protected float mLastMotionX;
protected float mLastMotionXRemainder;
protected float mLastMotionY;
protected float mTotalMotionX;
private int mLastScreenCenter = -1;
private int[] mChildOffsets;
private int[] mChildRelativeOffsets;
private int[] mChildOffsetsWithLayoutScale;
// 触摸滑动状态,没有状态,正在滑动,左滑条件达成,右滑条件达成
protected final static int TOUCH_STATE_REST = 0;
protected final static int TOUCH_STATE_SCROLLING = 1;
protected final static int TOUCH_STATE_PREV_PAGE = 2;
protected final static int TOUCH_STATE_NEXT_PAGE = 3;
protected final static float ALPHA_QUANTIZE_LEVEL = 0.0001f;
// 初始触摸状态为没有状态……
protected int mTouchState = TOUCH_STATE_REST;
protected boolean mForceScreenScrolled = false;
protected OnLongClickListener mLongClickListener;
protected boolean mAllowLongPress = true;
protected int mTouchSlop;
private int mPagingTouchSlop;
private int mMaximumVelocity;
private int mMinimumWidth;
protected int mPageSpacing;
protected int mPageLayoutPaddingTop;
protected int mPageLayoutPaddingBottom;
protected int mPageLayoutPaddingLeft;
protected int mPageLayoutPaddingRight;
protected int mPageLayoutWidthGap;
protected int mPageLayoutHeightGap;
protected int mCellCountX = 0;
protected int mCellCountY = 0;
protected boolean mCenterPagesVertically;
protected boolean mAllowOverScroll = true;
protected int mUnboundedScrollX;
protected int[] mTempVisiblePagesRange = new int[2];
protected boolean mForceDrawAllChildrenNextFrame;
// mOverScrollX is equal to getScrollX() when we're within the normal scroll
// range. Otherwise
// it is equal to the scaled overscroll position. We use a separate value so
// as to prevent
// the screens from continuing to translate beyond the normal bounds.
protected int mOverScrollX;
// parameter that adjusts the layout to be optimized for pages with that
// scale factor
protected float mLayoutScale = 1.0f;
protected static final int INVALID_POINTER = -1;
protected int mActivePointerId = INVALID_POINTER;
private PageSwitchListener mPageSwitchListener;
protected ArrayList<Boolean> mDirtyPageContent;
// 是否要刷新page
protected boolean mContentIsRefreshable = true;
// 是否在左右滑动时改变相邻page的alpha
protected boolean mFadeInAdjacentScreens = true;
// 是否使用一个不同的slop参数判定是否换到一个新page(pagingTouchSlop = 2 * touchSlop)
protected boolean mUsePagingTouchSlop = true;
// 子类是否在computeScroll方法里立刻更新scrollX
protected boolean mDeferScrollUpdate = false;
protected boolean mIsPageMoving = false;
// All syncs and layout passes are deferred until data is ready.
protected boolean mIsDataReady = false;
// Scrolling indicator
private ValueAnimator mScrollIndicatorAnimator;
private View mScrollIndicator;
private int mScrollIndicatorPaddingLeft;
private int mScrollIndicatorPaddingRight;
private boolean mHasScrollIndicator = true;
private boolean mShouldShowScrollIndicator = false;
private boolean mShouldShowScrollIndicatorImmediately = false;
protected static final int sScrollIndicatorFadeInDuration = 150;
protected static final int sScrollIndicatorFadeOutDuration = 650;
protected static final int sScrollIndicatorFlashDuration = 650;
private boolean mScrollingPaused = false;
// 是否直到滚动结束才加载相关页
private boolean mDeferLoadAssociatedPagesUntilScrollCompletes;
public interface PageSwitchListener {
void onPageSwitch(View newPage, int newPageIndex);
}
public PagedView(Context context) {
this(context, null);
}
public PagedView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public PagedView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
TypedArray a = context.obtainStyledAttributes(attrs,
R.styleable.PagedView, defStyle, 0);
setPageSpacing(a.getDimensionPixelSize(
R.styleable.PagedView_pageSpacing, 0));
mPageLayoutPaddingTop = a.getDimensionPixelSize(
R.styleable.PagedView_pageLayoutPaddingTop, 0);
mPageLayoutPaddingBottom = a.getDimensionPixelSize(
R.styleable.PagedView_pageLayoutPaddingBottom, 0);
mPageLayoutPaddingLeft = a.getDimensionPixelSize(
R.styleable.PagedView_pageLayoutPaddingLeft, 0);
mPageLayoutPaddingRight = a.getDimensionPixelSize(
R.styleable.PagedView_pageLayoutPaddingRight, 0);
mPageLayoutWidthGap = a.getDimensionPixelSize(
R.styleable.PagedView_pageLayoutWidthGap, 0);
mPageLayoutHeightGap = a.getDimensionPixelSize(
R.styleable.PagedView_pageLayoutHeightGap, 0);
mScrollIndicatorPaddingLeft = a.getDimensionPixelSize(
R.styleable.PagedView_scrollIndicatorPaddingLeft, 0);
mScrollIndicatorPaddingRight = a.getDimensionPixelSize(
R.styleable.PagedView_scrollIndicatorPaddingRight, 0);
a.recycle();
// 禁止触摸反馈
setHapticFeedbackEnabled(false);
init();
}
/**
* 初始化workspace参数和状态.
*/
protected void init() {
mDirtyPageContent = new ArrayList<Boolean>();
mDirtyPageContent.ensureCapacity(32);
mScroller = new Scroller(getContext(), new ScrollInterpolator());
mCurrentPage = 0;
mCenterPagesVertically = true;
final ViewConfiguration configuration = ViewConfiguration
.get(getContext());
mTouchSlop = configuration.getScaledTouchSlop();
mPagingTouchSlop = configuration.getScaledPagingTouchSlop();
mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
mDensity = getResources().getDisplayMetrics().density;
mFlingThresholdVelocity = (int) (FLING_THRESHOLD_VELOCITY * mDensity);
mMinFlingVelocity = (int) (MIN_FLING_VELOCITY * mDensity);
mMinSnapVelocity = (int) (MIN_SNAP_VELOCITY * mDensity);
// 侦听子控件的添加和移除事件
setOnHierarchyChangeListener(this);
}
public void setPageSwitchListener(PageSwitchListener pageSwitchListener) {
mPageSwitchListener = pageSwitchListener;
if (mPageSwitchListener != null) {
mPageSwitchListener.onPageSwitch(getPageAt(mCurrentPage),
mCurrentPage);
}
}
/**
* 由子类触发,标志着数据已经准备好,可以加载并而布局了
*/
protected void setDataIsReady() {
mIsDataReady = true;
}
protected boolean isDataReady() {
return mIsDataReady;
}
/**
* 返回当前页的Index
*/
int getCurrentPage() {
return mCurrentPage;
}
/**
* 返回下一页,如果下一页不存在,那么返回当前页
*/
int getNextPage() {
return (mNextPage != INVALID_PAGE) ? mNextPage : mCurrentPage;
}
/**
* 获取页面数量
*/
int getPageCount() {
return getChildCount();
}
/**
* 根据inde返回页面
*/
View getPageAt(int index) {
return getChildAt(index);
}
protected int indexToPage(int index) {
return index;
}
/**
* Updates the scroll of the current page immediately to its final scroll
* position. We use this in CustomizePagedView to allow tabs to share the
* same PagedView while resetting the scroll of the previous tab page.
* 将当前页立即滚动到目标位置。用于显示“所有程序”的情况下,当重置前一个tab包含的page的滚动状态时,允许tab们共用相同page。
*/
protected void updateCurrentPageScroll() {
// 如果当前页非法,那么直接滚动到0好了
int newX = 0;
if (0 <= mCurrentPage && mCurrentPage < getPageCount()) {
int offset = getChildOffset(mCurrentPage);
int relOffset = getRelativeChildOffset(mCurrentPage);
newX = offset - relOffset;
}
scrollTo(newX, 0);
mScroller.setFinalX(newX);
mScroller.forceFinished(true);
}
/**
* 当在“所有程序”和前台桌面间切换时,停止滚动,当其他动画完成时调用resumeScrolling
* 和updateCurrentPageScroll()以确保滚动完成并再次enable滚动
*/
void pauseScrolling() {
mScroller.forceFinished(true);
cancelScrollingIndicatorAnimations();
mScrollingPaused = true;
}
/**
* 使得page重新可以滚动
*
* @see #pauseScrolling()
*/
void resumeScrolling() {
mScrollingPaused = false;
}
/**
* 设置当前页
*/
void setCurrentPage(int currentPage) {
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
}
// 如果我们连默认值都改变了,那么不要加入任何像 mCurrentPage == currentPage这样的判断
if (getChildCount() == 0) {
return;
}
mCurrentPage = Math.max(0, Math.min(currentPage, getPageCount() - 1));
updateCurrentPageScroll();
updateScrollingIndicator();
notifyPageSwitchListener();
invalidate();
}
/**
* 通知页面切换完毕
*/
protected void notifyPageSwitchListener() {
if (mPageSwitchListener != null) {
mPageSwitchListener.onPageSwitch(getPageAt(mCurrentPage),
mCurrentPage);
}
}
/**
* 开始滚动页面
*/
protected void pageBeginMoving() {
if (!mIsPageMoving) {
mIsPageMoving = true;
onPageBeginMoving();
}
}
/**
* 停止滚动页面
*/
protected void pageEndMoving() {
if (mIsPageMoving) {
mIsPageMoving = false;
onPageEndMoving();
}
}
protected boolean isPageMoving() {
return mIsPageMoving;
}
// 由子类实现,以添加一些行为
protected void onPageBeginMoving() {
}
// 由子类实现,以添加一些行为
protected void onPageEndMoving() {
}
/**
* 为page分别注册长按侦听
*/
@Override
public void setOnLongClickListener(OnLongClickListener l) {
mLongClickListener = l;
final int count = getPageCount();
for (int i = 0; i < count; i++) {
getPageAt(i).setOnLongClickListener(l);
}
}
@Override
public void scrollBy(int x, int y) {
scrollTo(mUnboundedScrollX + x, getScrollY() + y);
}
@Override
public void scrollTo(int x, int y) {
mUnboundedScrollX = x;
if (x < 0) {
super.scrollTo(0, y);
if (mAllowOverScroll) {
overScroll(x);
}
} else if (x > mMaxScrollX) {
super.scrollTo(mMaxScrollX, y);
if (mAllowOverScroll) {
overScroll(x - mMaxScrollX);
}
} else {
mOverScrollX = x;
super.scrollTo(x, y);
}
mTouchX = x;
mSmoothingTime = System.nanoTime() / NANOTIME_DIV;
}
// we moved this functionality to a helper function so SmoothPagedView can
// reuse it
// 将这个功能加入到一个helper方法中以便SmoothPagedView可以重用它
protected boolean computeScrollHelper() {
if (mScroller.computeScrollOffset()) {
// 如果滚动尚未完毕
// 如果页面不需要滚动就别滚了(也就是页面的位置和当前正在计算的位置一样)
if (getScrollX() != mScroller.getCurrX()
|| getScrollY() != mScroller.getCurrY()
|| mOverScrollX != mScroller.getCurrX()) {
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
}
invalidate();
return true;
} else if (mNextPage != INVALID_PAGE) {
// 滚动完毕,通知事件
mCurrentPage = Math.max(0, Math.min(mNextPage, getPageCount() - 1));
mNextPage = INVALID_PAGE;
notifyPageSwitchListener();
// 如果可以,加载相关页面
if (mDeferLoadAssociatedPagesUntilScrollCompletes) {
loadAssociatedPages(mCurrentPage);
mDeferLoadAssociatedPagesUntilScrollCompletes = false;
}
// 只有滚动结束时(rest状态)才执行滚动停止事件
if (mTouchState == TOUCH_STATE_REST) {
pageEndMoving();
}
// 页面改变时通知用户。不懂什么意思 TODO
AccessibilityManager accessibilityManager = (AccessibilityManager) getContext()
.getSystemService(Context.ACCESSIBILITY_SERVICE);
if (accessibilityManager.isEnabled()) {
AccessibilityEvent ev = AccessibilityEvent
.obtain(AccessibilityEvent.TYPE_VIEW_SCROLLED);
ev.getText().add(getCurrentPageDescription());
sendAccessibilityEventUnchecked(ev);
}
return true;
}
return false;
}
@Override
public void computeScroll() {
computeScrollHelper();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (!mIsDataReady) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
return;
}
// 只有数据准备完毕时才测量
final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
final int widthSize = MeasureSpec.getSize(widthMeasureSpec);
final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
if (widthMode != MeasureSpec.EXACTLY) {
throw new IllegalStateException(
"Workspace can only be used in EXACTLY mode.");
}
// Return early if we aren't given a proper dimension
if (widthSize <= 0 || heightSize <= 0) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
return;
}
/*
* Allow the height to be set as WRAP_CONTENT. This allows the
* particular case of the All apps view on XLarge displays to not take
* up more space then it needs. Width is still not allowed to be set as
* WRAP_CONTENT since many parts of the code expect each page to have
* the same width. 高度允许自适应,但是宽度不允许,这是代码需要
*/
int maxChildHeight = 0;
final int verticalPadding = getPaddingTop() + getPaddingBottom();
final int horizontalPadding = getPaddingLeft() + getPaddingRight();
// The children are given the same width and height as the workspace
// unless they were set to WRAP_CONTENT
if (DEBUG)
Log.d(TAG, "PagedView.onMeasure(): " + widthSize + ", "
+ heightSize);
final int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
// 禁止page里面设置padding,0
final View child = getPageAt(i);
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
int childWidthMode;
if (lp.width == LayoutParams.WRAP_CONTENT) {
childWidthMode = MeasureSpec.AT_MOST;
} else {
childWidthMode = MeasureSpec.EXACTLY;
}
int childHeightMode;
if (lp.height == LayoutParams.WRAP_CONTENT) {
childHeightMode = MeasureSpec.AT_MOST;
} else {
childHeightMode = MeasureSpec.EXACTLY;
}
final int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
widthSize - horizontalPadding, childWidthMode);
final int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
heightSize - verticalPadding, childHeightMode);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
maxChildHeight = Math
.max(maxChildHeight, child.getMeasuredHeight());
if (DEBUG)
Log.d(TAG,
"\tmeasure-child" + i + ": " + child.getMeasuredWidth()
+ ", " + child.getMeasuredHeight());
}
if (heightMode == MeasureSpec.AT_MOST) {
heightSize = maxChildHeight + verticalPadding;
}
setMeasuredDimension(widthSize, heightSize);
// 调用setMeasuredDimension之前我们不能调用 getChildOffset/getRelativeChildOffset
// We also wait until we set the measured dimensions before flushing the
// cache as well, to
// ensure that the cache is filled with good values.
invalidateCachedOffsets();
if (childCount > 0) {
if (DEBUG)
Log.d(TAG, "getRelativeChildOffset(): " + getMeasuredWidth()
+ ", " + getChildWidth(0));
// 设定page之前的间隔(也就是page到屏幕的距离 )
if (mPageSpacing == AUTOMATIC_PAGE_SPACING) {
int offset = getRelativeChildOffset(0);
int spacing = Math.max(offset,
widthSize - offset - getChildAt(0).getMeasuredWidth());
setPageSpacing(spacing);
}
}
updateScrollingIndicatorPosition();
// TODO 回来再看
if (childCount > 0) {
mMaxScrollX = getChildOffset(childCount - 1)
- getRelativeChildOffset(childCount - 1);
} else {
mMaxScrollX = 0;
}
}
/**
* 直接滚动到指定页
*/
protected void scrollToNewPageWithoutMovingPages(int newCurrentPage) {
int newX = getChildOffset(newCurrentPage)
- getRelativeChildOffset(newCurrentPage);
int delta = newX - getScrollX();
final int pageCount = getChildCount();
for (int i = 0; i < pageCount; i++) {
View page = (View) getPageAt(i);
page.setX(page.getX() + delta);
}
setCurrentPage(newCurrentPage);
}
// 设定布局scale
public void setLayoutScale(float childrenScale) {
mLayoutScale = childrenScale;
invalidateCachedOffsets();
// 重新而已,但是要保留绝对之前的XY坐标
int childCount = getChildCount();
float childrenX[] = new float[childCount];
float childrenY[] = new float[childCount];
for (int i = 0; i < childCount; i++) {
final View child = getPageAt(i);
childrenX[i] = child.getX();
childrenY[i] = child.getY();
}
// Trigger a full re-layout (never just call onLayout directly!)
int widthSpec = MeasureSpec.makeMeasureSpec(getMeasuredWidth(),
MeasureSpec.EXACTLY);
int heightSpec = MeasureSpec.makeMeasureSpec(getMeasuredHeight(),
MeasureSpec.EXACTLY);
requestLayout();
measure(widthSpec, heightSpec);
layout(getLeft(), getTop(), getRight(), getBottom());
for (int i = 0; i < childCount; i++) {
final View child = getPageAt(i);
child.setX(childrenX[i]);
child.setY(childrenY[i]);
}
// 页面变小了,那么page offset就变了,所以要重新滚动一下。但是仍然要保持绝对XY坐标
scrollToNewPageWithoutMovingPages(mCurrentPage);
}
public void setPageSpacing(int pageSpacing) {
mPageSpacing = pageSpacing;
invalidateCachedOffsets();
}
@Override
protected void onLayout(boolean changed, int left, int top, int right,
int bottom) {
if (!mIsDataReady) {
return;
}
if (DEBUG)
Log.d(TAG, "PagedView.onLayout()");
final int verticalPadding = getPaddingTop() + getPaddingBottom();
final int childCount = getChildCount();
int childLeft = getRelativeChildOffset(0);
for (int i = 0; i < childCount; i++) {
final View child = getPageAt(i);
if (child.getVisibility() != View.GONE) {
final int childWidth = getScaledMeasuredWidth(child);
final int childHeight = child.getMeasuredHeight();
int childTop = getPaddingTop();
if (mCenterPagesVertically) {
childTop += ((getMeasuredHeight() - verticalPadding) - childHeight) / 2;
}
if (DEBUG)
Log.d(TAG, "\tlayout-child" + i + ": " + childLeft + ", "
+ childTop);
child.layout(childLeft, childTop,
childLeft + child.getMeasuredWidth(), childTop
+ childHeight);
childLeft += childWidth + mPageSpacing;
}
}
if (mFirstLayout && mCurrentPage >= 0 && mCurrentPage < getChildCount()) {
setHorizontalScrollBarEnabled(false);
updateCurrentPageScroll();
setHorizontalScrollBarEnabled(true);
mFirstLayout = false;
}
}
/**
* 滚动时修改indicator位置前修改相信page的alpha
*/
protected void screenScrolled(int screenCenter) {
if (isScrollingIndicatorEnabled()) {
updateScrollingIndicator();
}
boolean isInOverscroll = mOverScrollX < 0 || mOverScrollX > mMaxScrollX;
if (mFadeInAdjacentScreens && !isInOverscroll) {
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
if (child != null) {
float scrollProgress = getScrollProgress(screenCenter,
child, i);
float alpha = 1 - Math.abs(scrollProgress);
child.setAlpha(alpha);
}
}
invalidate();
}
}
@Override
public void onChildViewAdded(View parent, View child) {
// 确保当一个child被添加时,其大小alpha等要与当前滚动状态相符(比如在相信page里要变得透明并且变小)
mForceScreenScrolled = true;
invalidate();
invalidateCachedOffsets();
}
@Override
public void onChildViewRemoved(View parent, View child) {
}
/**
* 将缓存的各种offset重置
*/
protected void invalidateCachedOffsets() {
int count = getChildCount();
if (count == 0) {
mChildOffsets = null;
mChildRelativeOffsets = null;
mChildOffsetsWithLayoutScale = null;
return;
}
mChildOffsets = new int[count];
mChildRelativeOffsets = new int[count];
mChildOffsetsWithLayoutScale = new int[count];
for (int i = 0; i < count; i++) {
mChildOffsets[i] = -1;
mChildRelativeOffsets[i] = -1;
mChildOffsetsWithLayoutScale[i] = -1;
}
}
protected int getChildOffset(int index) {
int[] childOffsets = Float.compare(mLayoutScale, 1f) == 0 ? mChildOffsets
: mChildOffsetsWithLayoutScale;
if (childOffsets != null && childOffsets[index] != -1) {
return childOffsets[index];
} else {
if (getChildCount() == 0)
return 0;
int offset = getRelativeChildOffset(0);
for (int i = 0; i < index; ++i) {
offset += getScaledMeasuredWidth(getPageAt(i)) + mPageSpacing;
}
if (childOffsets != null) {
childOffsets[index] = offset;
}
return offset;
}
}
/**
* 获取page的位置
*/
protected int getRelativeChildOffset(int index) {
if (mChildRelativeOffsets != null && mChildRelativeOffsets[index] != -1) {
return mChildRelativeOffsets[index];
} else {
final int padding = getPaddingLeft() + getPaddingRight();
final int offset = getPaddingLeft()
+ (getMeasuredWidth() - padding - getChildWidth(index)) / 2;
if (mChildRelativeOffsets != null) {
mChildRelativeOffsets[index] = offset;
}
return offset;
}
}
/**
* 考虑缩放修正后的宽度
*/
protected int getScaledMeasuredWidth(View child) {
// This functions are called enough times that it actually makes a
// difference in the
// profiler -- so just inline the max() here
final int measuredWidth = child.getMeasuredWidth();
final int minWidth = mMinimumWidth;
final int maxWidth = (minWidth > measuredWidth) ? minWidth
: measuredWidth;
return (int) (maxWidth * mLayoutScale + 0.5f);
}
/**
* 获取可见的page,不明,莫非是参与变换的几个page,range是两头的值? TODO
*/
protected void getVisiblePages(int[] range) {
final int pageCount = getChildCount();
if (pageCount > 0) {
final int screenWidth = getMeasuredWidth();
int leftScreen = 0;
int rightScreen = 0;
View currPage = getPageAt(leftScreen);
while (leftScreen < pageCount - 1
&& currPage.getX() + currPage.getWidth()
- currPage.getPaddingRight() < getScrollX()) {
leftScreen++;
currPage = getPageAt(leftScreen);
}
rightScreen = leftScreen;
currPage = getPageAt(rightScreen + 1);
while (rightScreen < pageCount - 1
&& currPage.getX() - currPage.getPaddingLeft() < getScrollX()
+ screenWidth) {
rightScreen++;
currPage = getPageAt(rightScreen + 1);
}
range[0] = leftScreen;
range[1] = rightScreen;
} else {
range[0] = -1;
range[1] = -1;
}
}
protected boolean shouldDrawChild(View child) {
return child.getAlpha() > 0;
}
@Override
protected void dispatchDraw(Canvas canvas) {
int halfScreenSize = getMeasuredWidth() / 2;
// mOverScrollX is equal to getScrollX() when we're within the normal
// scroll range.
// Otherwise it is equal to the scaled overscroll position.
int screenCenter = mOverScrollX + halfScreenSize;
if (screenCenter != mLastScreenCenter || mForceScreenScrolled) {
// set mForceScreenScrolled before calling screenScrolled so that
// screenScrolled can
// set it for the next frame
mForceScreenScrolled = false;
screenScrolled(screenCenter);
mLastScreenCenter = screenCenter;
}
// Find out which screens are visible; as an optimization we only call
// draw on them
final int pageCount = getChildCount();
if (pageCount > 0) {
getVisiblePages(mTempVisiblePagesRange);
final int leftScreen = mTempVisiblePagesRange[0];
final int rightScreen = mTempVisiblePagesRange[1];
if (leftScreen != -1 && rightScreen != -1) {
final long drawingTime = getDrawingTime();
// Clip to the bounds
canvas.save();
canvas.clipRect(getScrollX(), getScrollY(), getScrollX()
+ getRight() - getLeft(), getScrollY() + getBottom()
- getTop());
for (int i = getChildCount() - 1; i >= 0; i--) {
final View v = getPageAt(i);
if (mForceDrawAllChildrenNextFrame
|| (leftScreen <= i && i <= rightScreen && shouldDrawChild(v))) {
drawChild(canvas, v, drawingTime);
}
}
mForceDrawAllChildrenNextFrame = false;
canvas.restore();
}
}
}
@Override
public boolean requestChildRectangleOnScreen(View child, Rect rectangle,
boolean immediate) {
int page = indexToPage(indexOfChild(child));
if (page != mCurrentPage || !mScroller.isFinished()) {
snapToPage(page);
return true;
}
return false;
}
@Override
protected boolean onRequestFocusInDescendants(int direction,
Rect previouslyFocusedRect) {
int focusablePage;
if (mNextPage != INVALID_PAGE) {
focusablePage = mNextPage;
} else {
focusablePage = mCurrentPage;
}
View v = getPageAt(focusablePage);
if (v != null) {
return v.requestFocus(direction, previouslyFocusedRect);
}
return false;
}
@Override
public boolean dispatchUnhandledMove(View focused, int direction) {
if (direction == View.FOCUS_LEFT) {
if (getCurrentPage() > 0) {
snapToPage(getCurrentPage() - 1);
return true;
}
} else if (direction == View.FOCUS_RIGHT) {
if (getCurrentPage() < getPageCount() - 1) {
snapToPage(getCurrentPage() + 1);
return true;
}
}
return super.dispatchUnhandledMove(focused, direction);
}
@Override
public void addFocusables(ArrayList<View> views, int direction,
int focusableMode) {
if (mCurrentPage >= 0 && mCurrentPage < getPageCount()) {
getPageAt(mCurrentPage).addFocusables(views, direction,
focusableMode);
}
if (direction == View.FOCUS_LEFT) {
if (mCurrentPage > 0) {
getPageAt(mCurrentPage - 1).addFocusables(views, direction,
focusableMode);
}
} else if (direction == View.FOCUS_RIGHT) {
if (mCurrentPage < getPageCount() - 1) {
getPageAt(mCurrentPage + 1).addFocusables(views, direction,
focusableMode);
}
}
}
/**
* If one of our descendant views decides that it could be focused now, only
* pass that along if it's on the current page.
*
* This happens when live folders requery, and if they're off page, they end
* up calling requestFocus, which pulls it on page.
*/
@Override
public void focusableViewAvailable(View focused) {
View current = getPageAt(mCurrentPage);
View v = focused;
while (true) {
if (v == current) {
super.focusableViewAvailable(focused);
return;
}
if (v == this) {
return;
}
ViewParent parent = v.getParent();
if (parent instanceof View) {
v = (View) v.getParent();
} else {
return;
}
}
}
/**
* {@inheritDoc}
*/
@Override
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
if (disallowIntercept) {
// 如果一个可滚动widget接管了touch事件,那么取消长按事件
final View currentPage = getPageAt(mCurrentPage);
currentPage.cancelLongPress();
}
super.requestDisallowInterceptTouchEvent(disallowIntercept);
}
/**
* page.如果一个tap可以触发flip到前一页的话,返回true
*/
protected boolean hitsPreviousPage(float x, float y) {
return (x < getRelativeChildOffset(mCurrentPage) - mPageSpacing);
}
/**
* page.如果一个tap可以触发flip到下一页的话,返回true
*/
protected boolean hitsNextPage(float x, float y) {
return (x > (getMeasuredWidth() - getRelativeChildOffset(mCurrentPage) + mPageSpacing));
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
/*
* 如果拦截,则处理滚动
*/
acquireVelocityTrackerAndAddMovement(ev);
// 没有child,直接走人不管了
if (getChildCount() <= 0)
return super.onInterceptTouchEvent(ev);
final int action = ev.getAction();
// 不处理滚动中的情况
if ((action == MotionEvent.ACTION_MOVE)
&& (mTouchState == TOUCH_STATE_SCROLLING)) {
return true;
}
switch (action & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_MOVE: {
/*
* 当前不是正在拖动的状态,而是要检查是否用户的手指已经动了足够远
*/
if (mActivePointerId != INVALID_POINTER) {
determineScrollingStart(ev);
break;
}
// 如果 mActivePointerId是INVALID_POINTER,
// 那么我们没有接收到DOWN事件,所以要把第一次move事件当成DOWN,因些这里没有break
// 当workspace变小的时候这就会发生
}
case MotionEvent.ACTION_DOWN: {
final float x = ev.getX();
final float y = ev.getY();
// Remember location of down touch
mDownMotionX = x;
mLastMotionX = x;
mLastMotionY = y;
mLastMotionXRemainder = 0;
mTotalMotionX = 0;
mActivePointerId = ev.getPointerId(0);
mAllowLongPress = true;
/*
* 当正在fling的时候按下,这时候应该启动drag
*/
final int xDist = Math.abs(mScroller.getFinalX()
- mScroller.getCurrX());
final boolean finishedScrolling = (mScroller.isFinished() || xDist < mTouchSlop);
if (finishedScrolling) {
mTouchState = TOUCH_STATE_REST;
mScroller.abortAnimation();
} else {
mTouchState = TOUCH_STATE_SCROLLING;
}
// 这次按下有可能导致滚动,如下
if (mTouchState != TOUCH_STATE_PREV_PAGE
&& mTouchState != TOUCH_STATE_NEXT_PAGE) {
if (getChildCount() > 0) {
if (hitsPreviousPage(x, y)) {
mTouchState = TOUCH_STATE_PREV_PAGE;
} else if (hitsNextPage(x, y)) {
mTouchState = TOUCH_STATE_NEXT_PAGE;
}
}
}
break;
}
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
mTouchState = TOUCH_STATE_REST;
mAllowLongPress = false;
mActivePointerId = INVALID_POINTER;
releaseVelocityTracker();
break;
// 不明 TODO
case MotionEvent.ACTION_POINTER_UP:
onSecondaryPointerUp(ev);
releaseVelocityTracker();
break;
}
/*
* 只要是在拖动状态就拦截事件
*/
return mTouchState != TOUCH_STATE_REST;
}
protected void determineScrollingStart(MotionEvent ev) {
determineScrollingStart(ev, 1.0f);
}
/*
* 根据手指移动距离判断是否可以开始滚动了
*/
protected void determineScrollingStart(MotionEvent ev, float touchSlopScale) {
/*
* Locally do absolute value. mLastMotionX is set to the y value of the
* down event.
*/
final int pointerIndex = ev.findPointerIndex(mActivePointerId);
if (pointerIndex == -1) {
return;
}
final float x = ev.getX(pointerIndex);
final float y = ev.getY(pointerIndex);
final int xDiff = (int) Math.abs(x - mLastMotionX);
final int yDiff = (int) Math.abs(y - mLastMotionY);
final int touchSlop = Math.round(touchSlopScale * mTouchSlop);
boolean xPaged = xDiff > mPagingTouchSlop;
boolean xMoved = xDiff > touchSlop;
boolean yMoved = yDiff > touchSlop;
if (xMoved || xPaged || yMoved) {
if (mUsePagingTouchSlop ? xPaged : xMoved) {
// 滑动到一定距离就滚动
mTouchState = TOUCH_STATE_SCROLLING;
mTotalMotionX += Math.abs(mLastMotionX - x);
mLastMotionX = x;
mLastMotionXRemainder = 0;
mTouchX = getScrollX();
mSmoothingTime = System.nanoTime() / NANOTIME_DIV;
pageBeginMoving();
}
// 取消任何可能存在的长按事件
cancelCurrentPageLongPress();
}
}
protected void cancelCurrentPageLongPress() {
if (mAllowLongPress) {
// 禁止其他任何地方调用的长按事件
mAllowLongPress = false;
final View currentPage = getPageAt(mCurrentPage);
if (currentPage != null) {
currentPage.cancelLongPress();
}
}
}
/**
* 返回滚动进度,比如用来更新indicator的位置
*/
protected float getScrollProgress(int screenCenter, View v, int page) {
final int halfScreenSize = getMeasuredWidth() / 2;
int totalDistance = getScaledMeasuredWidth(v) + mPageSpacing;
int delta = screenCenter
- (getChildOffset(page) - getRelativeChildOffset(page) + halfScreenSize);
float scrollProgress = delta / (totalDistance * 1.0f);
scrollProgress = Math.min(scrollProgress, 1.0f);
scrollProgress = Math.max(scrollProgress, -1.0f);
return scrollProgress;
}
// This curve determines how the effect of scrolling over the limits of the
// page dimishes
// as the user pulls further and further from the bounds
// 如果用户拉的过远如何处理
private float overScrollInfluenceCurve(float f) {
f -= 1.0f;
return f * f * f + 1.0f;
}
protected void acceleratedOverScroll(float amount) {
int screenSize = getMeasuredWidth();
// We want to reach the max over scroll effect when the user has
// over scrolled half the size of the screen
// 用户滚动的太多超过了半屏,就使用最大效果
float f = OVERSCROLL_ACCELERATE_FACTOR * (amount / screenSize);
if (f == 0)
return;
// Clamp this factor, f, to -1 < f < 1
if (Math.abs(f) >= 1) {
f /= Math.abs(f);
}
int overScrollAmount = (int) Math.round(f * screenSize);
if (amount < 0) {
mOverScrollX = overScrollAmount;
super.scrollTo(0, getScrollY());
} else {
mOverScrollX = mMaxScrollX + overScrollAmount;
super.scrollTo(mMaxScrollX, getScrollY());
}
invalidate();
}
protected void dampedOverScroll(float amount) {
int screenSize = getMeasuredWidth();
float f = (amount / screenSize);
if (f == 0)
return;
f = f / (Math.abs(f)) * (overScrollInfluenceCurve(Math.abs(f)));
// Clamp this factor, f, to -1 < f < 1
if (Math.abs(f) >= 1) {
f /= Math.abs(f);
}
int overScrollAmount = (int) Math.round(OVERSCROLL_DAMP_FACTOR * f
* screenSize);
if (amount < 0) {
mOverScrollX = overScrollAmount;
super.scrollTo(0, getScrollY());
} else {
mOverScrollX = mMaxScrollX + overScrollAmount;
super.scrollTo(mMaxScrollX, getScrollY());
}
invalidate();
}
protected void overScroll(float amount) {
dampedOverScroll(amount);
}
protected float maxOverScroll() {
// Using the formula in overScroll, assuming that f = 1.0 (which it
// should generally not
// exceed). Used to find out how much extra wallpaper we need for the
// over scroll effect
float f = 1.0f;
f = f / (Math.abs(f)) * (overScrollInfluenceCurve(Math.abs(f)));
return OVERSCROLL_DAMP_FACTOR * f;
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
// Skip touch handling if there are no pages to swipe
if (getChildCount() <= 0)
return super.onTouchEvent(ev);
acquireVelocityTrackerAndAddMovement(ev);
final int action = ev.getAction();
switch (action & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_DOWN:
/*
* If being flinged and user touches, stop the fling. isFinished
* will be false if being flinged.
*/
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
}
// Remember where the motion event started
mDownMotionX = mLastMotionX = ev.getX();
mLastMotionXRemainder = 0;
mTotalMotionX = 0;
mActivePointerId = ev.getPointerId(0);
if (mTouchState == TOUCH_STATE_SCROLLING) {
pageBeginMoving();
}
break;
case MotionEvent.ACTION_MOVE:
if (mTouchState == TOUCH_STATE_SCROLLING) {
// Scroll to follow the motion event
final int pointerIndex = ev.findPointerIndex(mActivePointerId);
final float x = ev.getX(pointerIndex);
final float deltaX = mLastMotionX + mLastMotionXRemainder - x;
mTotalMotionX += Math.abs(deltaX);
// Only scroll and update mLastMotionX if we have moved some
// discrete amount. We
// keep the remainder because we are actually testing if we've
// moved from the last
// scrolled position (which is discrete).
if (Math.abs(deltaX) >= 1.0f) {
mTouchX += deltaX;
mSmoothingTime = System.nanoTime() / NANOTIME_DIV;
if (!mDeferScrollUpdate) {
scrollBy((int) deltaX, 0);
if (DEBUG)
Log.d(TAG, "onTouchEvent().Scrolling: " + deltaX);
} else {
invalidate();
}
mLastMotionX = x;
mLastMotionXRemainder = deltaX - (int) deltaX;
} else {
awakenScrollBars();
}
} else {
determineScrollingStart(ev);
}
break;
case MotionEvent.ACTION_UP:
if (mTouchState == TOUCH_STATE_SCROLLING) {
final int activePointerId = mActivePointerId;
final int pointerIndex = ev.findPointerIndex(activePointerId);
final float x = ev.getX(pointerIndex);
final VelocityTracker velocityTracker = mVelocityTracker;
velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
int velocityX = (int) velocityTracker
.getXVelocity(activePointerId);
final int deltaX = (int) (x - mDownMotionX);
final int pageWidth = getScaledMeasuredWidth(getPageAt(mCurrentPage));
boolean isSignificantMove = Math.abs(deltaX) > pageWidth
* SIGNIFICANT_MOVE_THRESHOLD;
mTotalMotionX += Math.abs(mLastMotionX + mLastMotionXRemainder
- x);
boolean isFling = mTotalMotionX > MIN_LENGTH_FOR_FLING
&& Math.abs(velocityX) > mFlingThresholdVelocity;
// In the case that the page is moved far to one direction and
// then is flung
// in the opposite direction, we use a threshold to determine
// whether we should
// just return to the starting page, or if we should skip one
// further.
boolean returnToOriginalPage = false;
if (Math.abs(deltaX) > pageWidth
* RETURN_TO_ORIGINAL_PAGE_THRESHOLD
&& Math.signum(velocityX) != Math.signum(deltaX)
&& isFling) {
returnToOriginalPage = true;
}
int finalPage;
// We give flings precedence over large moves, which is why we
// short-circuit our
// test for a large move if a fling has been registered. That
// is, a large
// move to the left and fling to the right will register as a
// fling to the right.
if (((isSignificantMove && deltaX > 0 && !isFling) || (isFling && velocityX > 0))
&& mCurrentPage > 0) {
finalPage = returnToOriginalPage ? mCurrentPage
: mCurrentPage - 1;
snapToPageWithVelocity(finalPage, velocityX);
} else if (((isSignificantMove && deltaX < 0 && !isFling) || (isFling && velocityX < 0))
&& mCurrentPage < getChildCount() - 1) {
finalPage = returnToOriginalPage ? mCurrentPage
: mCurrentPage + 1;
snapToPageWithVelocity(finalPage, velocityX);
} else {
snapToDestination();
}
} else if (mTouchState == TOUCH_STATE_PREV_PAGE) {
// at this point we have not moved beyond the touch slop
// (otherwise mTouchState would be TOUCH_STATE_SCROLLING), so
// we can just page
int nextPage = Math.max(0, mCurrentPage - 1);
if (nextPage != mCurrentPage) {
snapToPage(nextPage);
} else {
snapToDestination();
}
} else if (mTouchState == TOUCH_STATE_NEXT_PAGE) {
// at this point we have not moved beyond the touch slop
// (otherwise mTouchState would be TOUCH_STATE_SCROLLING), so
// we can just page
int nextPage = Math.min(getChildCount() - 1, mCurrentPage + 1);
if (nextPage != mCurrentPage) {
snapToPage(nextPage);
} else {
snapToDestination();
}
} else {
onUnhandledTap(ev);
}
mTouchState = TOUCH_STATE_REST;
mActivePointerId = INVALID_POINTER;
releaseVelocityTracker();
break;
case MotionEvent.ACTION_CANCEL:
if (mTouchState == TOUCH_STATE_SCROLLING) {
snapToDestination();
}
mTouchState = TOUCH_STATE_REST;
mActivePointerId = INVALID_POINTER;
releaseVelocityTracker();
break;
case MotionEvent.ACTION_POINTER_UP:
onSecondaryPointerUp(ev);
break;
}
return true;
}
@Override
public boolean onGenericMotionEvent(MotionEvent event) {
if ((event.getSource() & InputDevice.SOURCE_CLASS_POINTER) != 0) {
switch (event.getAction()) {
case MotionEvent.ACTION_SCROLL: {
// Handle mouse (or ext. device) by shifting the page depending
// on the scroll
final float vscroll;
final float hscroll;
if ((event.getMetaState() & KeyEvent.META_SHIFT_ON) != 0) {
vscroll = 0;
hscroll = event.getAxisValue(MotionEvent.AXIS_VSCROLL);
} else {
vscroll = -event.getAxisValue(MotionEvent.AXIS_VSCROLL);
hscroll = event.getAxisValue(MotionEvent.AXIS_HSCROLL);
}
if (hscroll != 0 || vscroll != 0) {
if (hscroll > 0 || vscroll > 0) {
scrollRight();
} else {
scrollLeft();
}
return true;
}
}
}
}
return super.onGenericMotionEvent(event);
}
private void acquireVelocityTrackerAndAddMovement(MotionEvent ev) {
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();
}
mVelocityTracker.addMovement(ev);
}
private void releaseVelocityTracker() {
if (mVelocityTracker != null) {
mVelocityTracker.recycle();
mVelocityTracker = null;
}
}
/**
* 不明
*/
private void onSecondaryPointerUp(MotionEvent ev) {
final int pointerIndex = (ev.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) >> MotionEvent.ACTION_POINTER_INDEX_SHIFT;
final int pointerId = ev.getPointerId(pointerIndex);
if (pointerId == mActivePointerId) {
// This was our active pointer going up. Choose a new
// active pointer and adjust accordingly.
// TODO: Make this decision more intelligent.
final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
mLastMotionX = mDownMotionX = ev.getX(newPointerIndex);
mLastMotionY = ev.getY(newPointerIndex);
mLastMotionXRemainder = 0;
mActivePointerId = ev.getPointerId(newPointerIndex);
if (mVelocityTracker != null) {
mVelocityTracker.clear();
}
}
}
protected void onUnhandledTap(MotionEvent ev) {
}
@Override
public void requestChildFocus(View child, View focused) {
super.requestChildFocus(child, focused);
int page = indexToPage(indexOfChild(child));
if (page >= 0 && page != getCurrentPage() && !isInTouchMode()) {
snapToPage(page);
}
}
protected int getChildIndexForRelativeOffset(int relativeOffset) {
final int childCount = getChildCount();
int left;
int right;
for (int i = 0; i < childCount; ++i) {
left = getRelativeChildOffset(i);
right = (left + getScaledMeasuredWidth(getPageAt(i)));
if (left <= relativeOffset && relativeOffset <= right) {
return i;
}
}
return -1;
}
protected int getChildWidth(int index) {
// This functions are called enough times that it actually makes a
// difference in the
// profiler -- so just inline the max() here
final int measuredWidth = getPageAt(index).getMeasuredWidth();
final int minWidth = mMinimumWidth;
return (minWidth > measuredWidth) ? minWidth : measuredWidth;
}
int getPageNearestToCenterOfScreen() {
int minDistanceFromScreenCenter = Integer.MAX_VALUE;
int minDistanceFromScreenCenterIndex = -1;
int screenCenter = getScrollX() + (getMeasuredWidth() / 2);
final int childCount = getChildCount();
for (int i = 0; i < childCount; ++i) {
View layout = (View) getPageAt(i);
int childWidth = getScaledMeasuredWidth(layout);
int halfChildWidth = (childWidth / 2);
int childCenter = getChildOffset(i) + halfChildWidth;
int distanceFromScreenCenter = Math.abs(childCenter - screenCenter);
if (distanceFromScreenCenter < minDistanceFromScreenCenter) {
minDistanceFromScreenCenter = distanceFromScreenCenter;
minDistanceFromScreenCenterIndex = i;
}
}
return minDistanceFromScreenCenterIndex;
}
protected void snapToDestination() {
snapToPage(getPageNearestToCenterOfScreen(),
PAGE_SNAP_ANIMATION_DURATION);
}
private static class ScrollInterpolator implements Interpolator {
public ScrollInterpolator() {
}
public float getInterpolation(float t) {
t -= 1.0f;
return t * t * t * t * t + 1;
}
}
// We want the duration of the page snap animation to be influenced by the
// distance that
// the screen has to travel, however, we don't want this duration to be
// effected in a
// purely linear fashion. Instead, we use this method to moderate the effect
// that the distance
// of travel has on the overall snap duration.
float distanceInfluenceForSnapDuration(float f) {
f -= 0.5f; // center the values about 0.
f *= 0.3f * Math.PI / 2.0f;
return (float) Math.sin(f);
}
protected void snapToPageWithVelocity(int whichPage, int velocity) {
whichPage = Math.max(0, Math.min(whichPage, getChildCount() - 1));
int halfScreenSize = getMeasuredWidth() / 2;
if (DEBUG)
Log.d(TAG, "snapToPage.getChildOffset(): "
+ getChildOffset(whichPage));
if (DEBUG)
Log.d(TAG, "snapToPageWithVelocity.getRelativeChildOffset(): "
+ getMeasuredWidth() + ", " + getChildWidth(whichPage));
final int newX = getChildOffset(whichPage)
- getRelativeChildOffset(whichPage);
int delta = newX - mUnboundedScrollX;
int duration = 0;
if (Math.abs(velocity) < mMinFlingVelocity) {
// If the velocity is low enough, then treat this more as an
// automatic page advance
// as opposed to an apparent physical response to flinging
snapToPage(whichPage, PAGE_SNAP_ANIMATION_DURATION);
return;
}
// Here we compute a "distance" that will be used in the computation of
// the overall
// snap duration. This is a function of the actual distance that needs
// to be traveled;
// we keep this value close to half screen size in order to reduce the
// variance in snap
// duration as a function of the distance the page needs to travel.
float distanceRatio = Math.min(1f, 1.0f * Math.abs(delta)
/ (2 * halfScreenSize));
float distance = halfScreenSize + halfScreenSize
* distanceInfluenceForSnapDuration(distanceRatio);
velocity = Math.abs(velocity);
velocity = Math.max(mMinSnapVelocity, velocity);
// we want the page's snap velocity to approximately match the velocity
// at which the
// user flings, so we scale the duration by a value near to the
// derivative of the scroll
// interpolator at zero, ie. 5. We use 4 to make it a little slower.
duration = 4 * Math.round(1000 * Math.abs(distance / velocity));
duration = Math.min(duration, MAX_PAGE_SNAP_DURATION);
snapToPage(whichPage, delta, duration);
}
protected void snapToPage(int whichPage) {
snapToPage(whichPage, PAGE_SNAP_ANIMATION_DURATION);
}
protected void snapToPage(int whichPage, int duration) {
whichPage = Math.max(0, Math.min(whichPage, getPageCount() - 1));
if (DEBUG)
Log.d(TAG, "snapToPage.getChildOffset(): "
+ getChildOffset(whichPage));
if (DEBUG)
Log.d(TAG, "snapToPage.getRelativeChildOffset(): "
+ getMeasuredWidth() + ", " + getChildWidth(whichPage));
int newX = getChildOffset(whichPage)
- getRelativeChildOffset(whichPage);
final int sX = mUnboundedScrollX;
final int delta = newX - sX;
snapToPage(whichPage, delta, duration);
}
protected void snapToPage(int whichPage, int delta, int duration) {
mNextPage = whichPage;
View focusedChild = getFocusedChild();
if (focusedChild != null && whichPage != mCurrentPage
&& focusedChild == getPageAt(mCurrentPage)) {
focusedChild.clearFocus();
}
pageBeginMoving();
awakenScrollBars(duration);
if (duration == 0) {
duration = Math.abs(delta);
}
if (!mScroller.isFinished())
mScroller.abortAnimation();
mScroller.startScroll(mUnboundedScrollX, 0, delta, 0, duration);
// Load associated pages immediately if someone else is handling the
// scroll, otherwise defer
// loading associated pages until the scroll settles
if (mDeferScrollUpdate) {
loadAssociatedPages(mNextPage);
} else {
mDeferLoadAssociatedPagesUntilScrollCompletes = true;
}
notifyPageSwitchListener();
invalidate();
}
public void scrollLeft() {
if (mScroller.isFinished()) {
if (mCurrentPage > 0)
snapToPage(mCurrentPage - 1);
} else {
if (mNextPage > 0)
snapToPage(mNextPage - 1);
}
}
public void scrollRight() {
if (mScroller.isFinished()) {
if (mCurrentPage < getChildCount() - 1)
snapToPage(mCurrentPage + 1);
} else {
if (mNextPage < getChildCount() - 1)
snapToPage(mNextPage + 1);
}
}
public int getPageForView(View v) {
int result = -1;
if (v != null) {
ViewParent vp = v.getParent();
int count = getChildCount();
for (int i = 0; i < count; i++) {
if (vp == getPageAt(i)) {
return i;
}
}
}
return result;
}
/**
* @return True is long presses are still allowed for the current touch
*/
public boolean allowLongPress() {
return mAllowLongPress;
}
/**
* Set true to allow long-press events to be triggered, usually checked by
* {@link Launcher} to accept or block dpad-initiated long-presses.
*/
public void setAllowLongPress(boolean allowLongPress) {
mAllowLongPress = allowLongPress;
}
/**
* 没用
*/
public static class SavedState extends BaseSavedState {
int currentPage = -1;
SavedState(Parcelable superState) {
super(superState);
}
private SavedState(Parcel in) {
super(in);
currentPage = in.readInt();
}
@Override
public void writeToParcel(Parcel out, int flags) {
super.writeToParcel(out, flags);
out.writeInt(currentPage);
}
public static final Parcelable.Creator<SavedState> CREATOR = new Parcelable.Creator<SavedState>() {
public SavedState createFromParcel(Parcel in) {
return new SavedState(in);
}
public SavedState[] newArray(int size) {
return new SavedState[size];
}
};
}
protected void loadAssociatedPages(int page) {
loadAssociatedPages(page, false);
}
/**
* 加载相关页,也就是当前页和它的相邻页
*/
protected void loadAssociatedPages(int page, boolean immediateAndOnly) {
if (mContentIsRefreshable) {
final int count = getChildCount();
if (page < count) {
int lowerPageBound = getAssociatedLowerPageBound(page);
int upperPageBound = getAssociatedUpperPageBound(page);
if (DEBUG)
Log.d(TAG, "loadAssociatedPages: " + lowerPageBound + "/"
+ upperPageBound);
// First, clear any pages that should no longer be loaded
for (int i = 0; i < count; ++i) {
Page layout = (Page) getPageAt(i);
if ((i < lowerPageBound) || (i > upperPageBound)) {
if (layout.getPageChildCount() > 0) {
layout.removeAllViewsOnPage();
}
mDirtyPageContent.set(i, true);
}
}
// Next, load any new pages
for (int i = 0; i < count; ++i) {
if ((i != page) && immediateAndOnly) {
continue;
}
if (lowerPageBound <= i && i <= upperPageBound) {
if (mDirtyPageContent.get(i)) {
syncPageItems(i, (i == page) && immediateAndOnly);
mDirtyPageContent.set(i, false);
}
}
}
}
}
}
protected int getAssociatedLowerPageBound(int page) {
return Math.max(0, page - 1);
}
protected int getAssociatedUpperPageBound(int page) {
final int count = getChildCount();
return Math.min(page + 1, count - 1);
}
/**
* This method is called ONLY to synchronize the number of pages that the
* paged view has. To actually fill the pages with information, implement
* syncPageItems() below. It is guaranteed that syncPageItems() will be
* called for a particular page before it is shown, and therefore,
* individual page items do not need to be updated in this method.
*/
public abstract void syncPages();
/**
* This method is called to synchronize the items that are on a particular
* page. If views on the page can be reused, then they should be updated
* within this method.
*/
public abstract void syncPageItems(int page, boolean immediate);
protected void invalidatePageData() {
invalidatePageData(-1, false);
}
protected void invalidatePageData(int currentPage) {
invalidatePageData(currentPage, false);
}
protected void invalidatePageData(int currentPage, boolean immediateAndOnly) {
if (!mIsDataReady) {
return;
}
if (mContentIsRefreshable) {
// Force all scrolling-related behavior to end
mScroller.forceFinished(true);
mNextPage = INVALID_PAGE;
// Update all the pages
syncPages();
// We must force a measure after we've loaded the pages to update
// the content width and
// to determine the full scroll width
measure(MeasureSpec.makeMeasureSpec(getMeasuredWidth(),
MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(
getMeasuredHeight(), MeasureSpec.EXACTLY));
// Set a new page as the current page if necessary
if (currentPage > -1) {
setCurrentPage(Math.min(getPageCount() - 1, currentPage));
}
// Mark each of the pages as dirty
final int count = getChildCount();
mDirtyPageContent.clear();
for (int i = 0; i < count; ++i) {
mDirtyPageContent.add(true);
}
// Load any pages that are necessary for the current window of views
loadAssociatedPages(mCurrentPage, immediateAndOnly);
requestLayout();
}
}
protected View getScrollingIndicator() {
// We use mHasScrollIndicator to prevent future lookups if there is no
// sibling indicator
// found
if (mHasScrollIndicator && mScrollIndicator == null) {
ViewGroup parent = (ViewGroup) getParent();
if (parent != null) {
mScrollIndicator = (View) (parent
.findViewById(R.id.paged_view_indicator));
mHasScrollIndicator = mScrollIndicator != null;
if (mHasScrollIndicator) {
mScrollIndicator.setVisibility(View.VISIBLE);
}
}
}
return mScrollIndicator;
}
protected boolean isScrollingIndicatorEnabled() {
return true;
}
Runnable hideScrollingIndicatorRunnable = new Runnable() {
@Override
public void run() {
hideScrollingIndicator(false);
}
};
protected void flashScrollingIndicator(boolean animated) {
removeCallbacks(hideScrollingIndicatorRunnable);
showScrollingIndicator(!animated);
postDelayed(hideScrollingIndicatorRunnable,
sScrollIndicatorFlashDuration);
}
protected void showScrollingIndicator(boolean immediately) {
mShouldShowScrollIndicator = true;
mShouldShowScrollIndicatorImmediately = true;
if (getChildCount() <= 1)
return;
if (!isScrollingIndicatorEnabled())
return;
mShouldShowScrollIndicator = false;
getScrollingIndicator();
if (mScrollIndicator != null) {
// Fade the indicator in
updateScrollingIndicatorPosition();
mScrollIndicator.setVisibility(View.VISIBLE);
cancelScrollingIndicatorAnimations();
if (immediately || mScrollingPaused) {
mScrollIndicator.setAlpha(1f);
} else {
mScrollIndicatorAnimator = LauncherAnimUtils.ofFloat(
mScrollIndicator, "alpha", 1f);
mScrollIndicatorAnimator
.setDuration(sScrollIndicatorFadeInDuration);
mScrollIndicatorAnimator.start();
}
}
}
protected void cancelScrollingIndicatorAnimations() {
if (mScrollIndicatorAnimator != null) {
mScrollIndicatorAnimator.cancel();
}
}
protected void hideScrollingIndicator(boolean immediately) {
if (getChildCount() <= 1)
return;
if (!isScrollingIndicatorEnabled())
return;
getScrollingIndicator();
if (mScrollIndicator != null) {
// Fade the indicator out
updateScrollingIndicatorPosition();
cancelScrollingIndicatorAnimations();
if (immediately || mScrollingPaused) {
mScrollIndicator.setVisibility(View.INVISIBLE);
mScrollIndicator.setAlpha(0f);
} else {
mScrollIndicatorAnimator = LauncherAnimUtils.ofFloat(
mScrollIndicator, "alpha", 0f);
mScrollIndicatorAnimator
.setDuration(sScrollIndicatorFadeOutDuration);
mScrollIndicatorAnimator
.addListener(new AnimatorListenerAdapter() {
private boolean cancelled = false;
@Override
public void onAnimationCancel(
android.animation.Animator animation) {
cancelled = true;
}
@Override
public void onAnimationEnd(Animator animation) {
if (!cancelled) {
mScrollIndicator
.setVisibility(View.INVISIBLE);
}
}
});
mScrollIndicatorAnimator.start();
}
}
}
/**
* To be overridden by subclasses to determine whether the scroll indicator
* should stretch to fill its space on the track or not.
*/
protected boolean hasElasticScrollIndicator() {
return true;
}
private void updateScrollingIndicator() {
if (getChildCount() <= 1)
return;
if (!isScrollingIndicatorEnabled())
return;
getScrollingIndicator();
if (mScrollIndicator != null) {
updateScrollingIndicatorPosition();
}
if (mShouldShowScrollIndicator) {
showScrollingIndicator(mShouldShowScrollIndicatorImmediately);
}
}
private void updateScrollingIndicatorPosition() {
if (!isScrollingIndicatorEnabled())
return;
if (mScrollIndicator == null)
return;
int numPages = getChildCount();
int pageWidth = getMeasuredWidth();
int lastChildIndex = Math.max(0, getChildCount() - 1);
int maxScrollX = getChildOffset(lastChildIndex)
- getRelativeChildOffset(lastChildIndex);
int trackWidth = pageWidth - mScrollIndicatorPaddingLeft
- mScrollIndicatorPaddingRight;
int indicatorWidth = mScrollIndicator.getMeasuredWidth()
- mScrollIndicator.getPaddingLeft()
- mScrollIndicator.getPaddingRight();
float offset = Math.max(0f,
Math.min(1f, (float) getScrollX() / maxScrollX));
int indicatorSpace = trackWidth / numPages;
int indicatorPos = (int) (offset * (trackWidth - indicatorSpace))
+ mScrollIndicatorPaddingLeft;
if (hasElasticScrollIndicator()) {
if (mScrollIndicator.getMeasuredWidth() != indicatorSpace) {
mScrollIndicator.getLayoutParams().width = indicatorSpace;
mScrollIndicator.requestLayout();
}
} else {
int indicatorCenterOffset = indicatorSpace / 2 - indicatorWidth / 2;
indicatorPos += indicatorCenterOffset;
}
mScrollIndicator.setTranslationX(indicatorPos);
}
public void showScrollIndicatorTrack() {
}
public void hideScrollIndicatorTrack() {
}
/* Accessibility */
@Override
public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
super.onInitializeAccessibilityNodeInfo(info);
info.setScrollable(getPageCount() > 1);
if (getCurrentPage() < getPageCount() - 1) {
info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD);
}
if (getCurrentPage() > 0) {
info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD);
}
}
@Override
public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
super.onInitializeAccessibilityEvent(event);
event.setScrollable(true);
if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_SCROLLED) {
event.setFromIndex(mCurrentPage);
event.setToIndex(mCurrentPage);
event.setItemCount(getChildCount());
}
}
@Override
public boolean performAccessibilityAction(int action, Bundle arguments) {
if (super.performAccessibilityAction(action, arguments)) {
return true;
}
switch (action) {
case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD: {
if (getCurrentPage() < getPageCount() - 1) {
scrollRight();
return true;
}
}
break;
case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD: {
if (getCurrentPage() > 0) {
scrollLeft();
return true;
}
}
break;
}
return false;
}
protected String getCurrentPageDescription() {
return String.format(
getContext().getString(R.string.default_scroll_format),
getNextPage() + 1, getChildCount());
}
@Override
public boolean onHoverEvent(android.view.MotionEvent event) {
return true;
}
}